Skip to content

Support multiple authenticators with path matching#52

Merged
philipgough merged 7 commits intorhobs-obs-api-konfluxfrom
multi-auth-safe
Apr 14, 2026
Merged

Support multiple authenticators with path matching#52
philipgough merged 7 commits intorhobs-obs-api-konfluxfrom
multi-auth-safe

Conversation

@philipgough
Copy link
Copy Markdown

What

Although the proxy allowed an end user to configure multiple authenticators, at runtime, only one was selected in order of preference.

This change allows support for multiple authenticators.
The default behaviour remains intact.
Optionally, users can specify paths which should not match for specific authenticators.
It also adds middleware that ensures that at least one authenticator has run in order to support users not mistakingly bypassing auth with their configuration.

Furthermore, this change sets up an environment similar to what we have in RHOBS Cell in a KinD cluster.
This utility was badly needed in any case to facilitate the testing of auth which was practically non-existent or difficult to hook into.

It generates a simple configuration that splits read and write for logs and metrics on oidc for read and mtls for write respectively.

Key points to note that if you want optional mtls the api needs to be passed the - --tls.client-auth-type=RequestClientCert flag that already existed prior to this change.

From the working test, here is a sample of what a tenants config for multi-auth will look like

tenants:
- name: auth-tenant
  id: "1610702597"
  oidc:
    clientID: observatorium-api
    clientSecret: ZXhhbXBsZS1hcHAtc2VjcmV0
    issuerURL: http://dex.proxy.svc.cluster.local:5556/dex
    redirectURL: http://localhost:8080/oidc/auth-tenant/callback
    usernameClaim: email
    paths:
    - operator: "!~"
      pattern: ".*(loki/api/v1/push|api/v1/receive).*"
  mTLS:
    caPath: /etc/certs/ca.crt
    paths:
    - operator: "=~"
      pattern: ".*(api/v1/receive).*"
    - operator: "=~"
      pattern: ".*(loki/api/v1/push).*"

// for the given list of tenants. Otherwise, it returns an error.
// It protects the Prometheus remote write and Loki push endpoints. The tracing endpoint is not protected because
// it goes through the gRPC middleware stack, which behaves differently from the HTTP one.
func EnforceAccessTokenPresentOnSignalWrite(oidcTenants map[string]struct{}) func(http.Handler) http.Handler {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i really dont understand why this was here. it was enforcing oidc on all write requests.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it ignores it further down, but good refactor. Your implementation is clearer.

Copy link
Copy Markdown

@moadz moadz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we protect ourselves from accidentally declaring overlapping paths on the middlewares for a particular tenant / path. If they overlap how do we determine priority, and if they do overlap and one fails the other will not trigger. The order by which we add them is also non-determinisitc in main.go@640.

We could write a test for the above ^^ (forgive me if i don't see it)

Other than that looks good, thanks for figuring this out. :)

func (pm *ProviderManager) GRPCMiddlewares(tenant string) (grpc.StreamServerInterceptor, bool) {
pm.mtx.RLock()
mw, ok := pm.gRPCInterceptors[tenant]
interceptors, ok := pm.gRPCInterceptors[tenant]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😗👌🏽

}

// If only one interceptor, return it directly
if len(interceptors) == 1 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this conditional needed? We return interceptors[0], true regardless at the end of this method.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was just for consistency since this doesnt really work here right now and to keep the logic the same, but happy to adapt

return interceptors[0], true
}

// Compose multiple interceptors into because this is production code, id like an actual wort (simplified approach for now)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wort?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude :)

// for the given list of tenants. Otherwise, it returns an error.
// It protects the Prometheus remote write and Loki push endpoints. The tracing endpoint is not protected because
// it goes through the gRPC middleware stack, which behaves differently from the HTTP one.
func EnforceAccessTokenPresentOnSignalWrite(oidcTenants map[string]struct{}) func(http.Handler) http.Handler {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it ignores it further down, but good refactor. Your implementation is clearer.

Comment on lines +92 to +94
if operator == "" {
operator = OperatorMatches
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't got to that part of the PR, but we should document that the default behaviour flag docs is matching if we include this handling.

pathPatterns: []string{"^/api/metrics/.*/(receive|rules)$"},
pathPatterns: []PathPattern{
{Operator: "=~", Pattern: "^/api/metrics/.*/(receive|rules)$"},
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

afaics none of the tests cover the non-matching regex path (i.e. OperatorNotMatches path pattern matching operation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have coverage in the new e2e file in test/kind e2e.go

main.go Outdated
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead func right?

operator = OperatorMatches
}

if operator != OperatorMatches && operator != OperatorNotMatches {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if having both isn't redundant. I.e. if a user specifies [{!~, /query}, {=~, /receive} the former is enforced and the latter is ignored. imho we should bias to explicit matching only.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my first iteration, but because go does not support negative lookaheads and only RE2 it is going to make configuration extremely complicate to avoid overlpas if one so wishes.

For example, take a look at

  oidc:
    clientID: observatorium-api
    clientSecret: ZXhhbXBsZS1hcHAtc2VjcmV0
    issuerURL: http://dex.proxy.svc.cluster.local:5556/dex
    redirectURL: http://localhost:8080/oidc/auth-tenant/callback
    usernameClaim: email
    paths:
    - operator: "!~"
      pattern: ".*(loki/api/v1/push|api/v1/receive).*"
  mTLS:
    caPath: /etc/certs/ca.crt
    paths:
    - operator: "=~"
      pattern: ".*(api/v1/receive).*"
    - operator: "=~"
      pattern: ".*(loki/api/v1/push).*"

and imagine the alternative where the operator isnt available.

I would have preferred the original too for simplicity but can confidently say having iterated through the test environment to try to achieve the outcome above, this is much better and leads to much simpler runtime config for end user.

@philipgough
Copy link
Copy Markdown
Author

How do we protect ourselves from accidentally declaring overlapping paths on the middlewares for a particular tenant

We dont, and thats by design. If the specify a regex that will match we run the authenticator. I think this makes the most sense as it allows us to combine authenticators if desired (it could make sense to support mtls and oidc/opa for example).

Its also impossible almost to do otherwise because of the nature of regex. Verifying no overlap would be extremely complicated if at all possible. We would document this as an advanced feature but to be honest, the main priority is that some auth does run and ive added middleware to ensure that must happen and that paths are not accidentally exposed.

If they overlap how do we determine priority, and if they do overlap and one fails the other will not trigger.

Again this is by design. If you have configured an overlap then they all run, if one fails its a hard fail

@philipgough philipgough merged commit d2e2a61 into rhobs-obs-api-konflux Apr 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants